package com.stark.commons.lang.util;

import lombok.Cleanup;
import lombok.NoArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.net.ftp.*;

import java.io.*;
import java.util.Arrays;

/**
 * FTP 客户端。
 * @author Ben
 * @since 1.0.0
 * @version 1.0.0
 */
@SuppressWarnings("unused")
@NoArgsConstructor
public class FtpClient {

	private FTPClient ftp;
	private String server;
	private Integer port;
	private String username;
	private String password;
	private String localCharset = "GBK";            // 如果服务器支持就使用 UTF-8 ，否则使用本地编码，默认 GBK
	private static final String SERVER_CHARSET = "ISO-8859-1";    // FTP 协议使用 ISO-8859-1 传输

	/**
	 * 获取 FTP 服务器信息， IP 和端口。
	 * @return FTP 服务器信息， IP 和端口。
	 */
	public String getServerInfo() {
		return "{"
				+ "\"server\":\"" + server + "\","
				+ "\"port\":" + port + ","
				+ "\"username\":\"" + username + "\","
				+ "\"password\":\"" + password + "\""
				+ "}";
	}

	/**
	 * 连接FTP服务器。
	 * @param server 服务器地址。
	 * @param port 端口。
	 * @param username 用户名。
	 * @param password 密码。
	 * @throws IOException 建立连接时抛出异常。
	 */
	public void connect(String server, int port, String username, String password)
			throws IOException {
		this.server = server;
		this.port = port;
		this.username = username;
		this.password = password;

		boolean flag = false;
		try {
			ftp = new FTPClient();
			ftp.connect(server, port);
			int reply = ftp.getReplyCode();
			if (!FTPReply.isPositiveCompletion(reply)) {
				ftp.disconnect();
				throw new IOException("连接服务器失败: " + getServerInfo() + ", 错误信息: " + reply + "-" + getMsgByCode(reply));
			}
			if (FTPReply.isPositiveCompletion(ftp.sendCommand("OPTS UTF8", "ON"))) {    // 询问服务器是否支持 UTF-8 编码
				localCharset = "UTF-8";
			}
			FTPClientConfig config = new FTPClientConfig();
			config.setDefaultDateFormatStr("yyyy-MM-dd HH:mm:ss");
			ftp.configure(config);
			ftp.setControlEncoding(localCharset);

			if (!ftp.login(username, password)) {
				throw new IOException("登录失败: " + getServerInfo());
			}
			ftp.enterLocalPassiveMode();
			ftp.setFileType(FTP.BINARY_FILE_TYPE);
			flag = true;
		} finally {
			if (!flag && ftp.isConnected()) {
				try {
					ftp.disconnect();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		}
	}

	/**
	 * 是否连接中。
	 * @return 连接中返回 {@literal true} ，否则返回 {@literal false} 。
	 */
	public boolean isConnected() {
		return ftp != null && ftp.isConnected();
	}

	/**
	 * 断开FTP服务器。
	 */
	public void disconnect() {
		if (isConnected()) {
			try {
				ftp.logout();
				ftp.disconnect();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}

	/**
	 * 列举指定路径下所有文件。
	 * @param dir 相对目录。
	 * @return 文件名称列表。
	 */
	public String[] listFiles(String dir) throws IOException {
		try {
			changeWorkingDirectory(dir);
		} catch (Exception e) {
			return new String[0];
		}

		FTPFile[] files = ftp.listFiles();
		return Arrays.stream(files)
				.filter(FTPFile::isFile)
				.map(FTPFile::getName).toArray(String[]::new);
	}

	/**
	 * 文件是否存在。
	 * @param dir 相对目录。
	 * @param filename 文件名。
	 * @return 存在返回 {@literal true} ，否则返回 {@literal false} 。
	 */
	public boolean exists(String dir, String filename) throws IOException {
		for (String each : listFiles(dir)) {
			if (each.equals(filename)) {
				return true;
			}
		}
		return false;
	}

	/**
	 * 上传文件。
	 * @param dir 相对目录。
	 * @param filename 文件名称，包含后缀。
	 * @param bytes 文件字节码。
	 */
	public void uploadFile(String dir, String filename, byte[] bytes) throws IOException {
		changeWorkingDirectory("/");
		mkdirs(dir);

		@Cleanup InputStream in = new ByteArrayInputStream(bytes);
		if (!ftp.storeFile(changeEncoding(filename), in)) {
			int reply = ftp.getReplyCode();
			throw new IOException("上传文件失败: " + getServerInfo()
					+ ",dir=" + dir
					+ ",filename=" + filename
					+ ",errmsg=" + reply + "-" + getMsgByCode(reply));
		}
	}

	/**
	 * 下载文件。
	 * @param dir 相对目录。
	 * @param filename 文件名称。
	 * @return 文件字节码。
	 */
	public byte[] downloadFile(String dir, String filename) throws IOException {
		changeWorkingDirectory(dir);

		@Cleanup ByteArrayOutputStream out = new ByteArrayOutputStream();
		if (!ftp.retrieveFile(changeEncoding(filename), out)) {
			int reply = ftp.getReplyCode();
			throw new IOException("下载文件失败: " + getServerInfo()
					+ ",dir=" + dir
					+ ",filename=" + filename
					+ ",errmsg=" + reply + "-" + getMsgByCode(reply));
		}
		return out.toByteArray();
	}

	/**
	 * 下载文件。
	 * @param dir 相对目录。
	 * @param filename 文件名称。
	 * @param localDir 本地保存路径。
	 * @param localFilename 本地保存名称。
	 */
	public void downloadFile(String dir, String filename, String localDir, String localFilename)
			throws IOException {
		changeWorkingDirectory(dir);

		File file = FileUtils.createFile(localDir + File.separator + localFilename);
		@Cleanup FileOutputStream out = new FileOutputStream(file);
		if (!ftp.retrieveFile(changeEncoding(filename), out)) {
			int reply = ftp.getReplyCode();
			throw new IOException("下载文件失败: " + getServerInfo()
					+ ",dir=" + dir
					+ ",filename=" + filename
					+ ",errmsg=" + reply + "-" + getMsgByCode(reply));
		}
	}

	/**
	 * 下载文件。
	 * @param dir 相对目录。
	 * @param filename 文件名称。
	 * @param localDir 本地保存路径。
	 */
	public void downloadFile(String dir, String filename, String localDir)
			throws IOException {
		downloadFile(dir, filename, localDir, filename);
	}

	/**
	 * 下载文件夹。
	 * @param dir 相对目录。
	 * @param localDir 本地存储目录。
	 */
	public void downloadDirectory(String dir, String localDir) throws IOException {
		changeWorkingDirectory(dir);
		FileUtils.createDirectory(localDir);
		for (FTPFile file : ftp.listFiles()) {
			if (file.isDirectory()) {
				downloadDirectory(dir + "/" + file.getName(), localDir + "/" + file.getName());
			} else {
				downloadFile(dir, file.getName(), localDir);
			}
		}
	}

	/**
	 * 创建目录。
	 * @param dir 相对目录。
	 */
	public void mkdirs(String dir) throws IOException {
		String[] dirArray = formatPath(dir).split("/");
		for (String dirItem : dirArray) {
			if (StringUtils.isNotBlank(dirItem)) {
				dirItem = changeEncoding(dirItem);
				if (!ftp.changeWorkingDirectory(dirItem)) {
					if (!ftp.makeDirectory(dirItem)
							|| !ftp.changeWorkingDirectory(dirItem)) {
						int reply = ftp.getReplyCode();
						throw new IOException("创建目录失败: " + getServerInfo()
								+ ",dir=" + dir
								+ ",errmsg=" + reply + "-" + getMsgByCode(reply));
					}
				}
			}
		}
	}

	/**
	 * 删除文件。
	 * @param dir 相对目录。
	 * @param filename 文件名称（包含后缀）。
	 */
	public void deleteFile(String dir, String filename) throws IOException {
		try {
			changeWorkingDirectory(dir);
		} catch (Exception e) {
			return;
		}

		if (!ftp.deleteFile(changeEncoding(filename))) {
			int reply = ftp.getReplyCode();
			throw new IOException("删除文件失败: " + getServerInfo()
					+ ",dir=" + dir
					+ ",filename=" + filename
					+ ",errmsg=" + reply + "-" + getMsgByCode(reply));
		}
	}

	/**
	 * 重命名文件夹或文件。
	 * @param dir 相对目录。
	 * @param sourceName 原文件夹名或文件名。
	 * @param destName 新文件夹名或文件名。
	 */
	public void rename(String dir, String sourceName, String destName) throws IOException {
		if (!ftp.rename(changeEncoding(dir + "/" + sourceName), changeEncoding(dir + "/" + destName))) {
			int reply = ftp.getReplyCode();
			throw new IOException("重命名失败: " + getServerInfo()
					+ ",dir=" + dir
					+ ",sourceName=" + sourceName
					+ ",destName=" + destName
					+ ",errmsg=" + reply + "-" + getMsgByCode(reply));
		}
	}

	/**
	 * 切换工作目录。
	 * @param dir 相对目录。
	 */
	private void changeWorkingDirectory(String dir) throws IOException {
		if (!ftp.changeWorkingDirectory(changeEncoding(formatPath(dir)))) {
			String _localCharset = localCharset;
			if ("UTF-8".equals(_localCharset)) {
				localCharset = "GBK";
			} else {
				localCharset = "UTF-8";
			}
			if (!ftp.changeWorkingDirectory(changeEncoding(formatPath(dir)))) {
				localCharset = _localCharset;
				int reply = ftp.getReplyCode();
				throw new IOException("切换目录失败: " + getServerInfo()
						+ ",dir=" + dir
						+ ",errmsg=" + reply + "-" + getMsgByCode(reply));
			}
		}
	}

	/**
	 * 格式化路径，兼容各个系统。
	 * @param path 路径。
	 * @return 格式化后的路径。
	 */
	private String formatPath(String path) {
		path = path.replace("\\", "/");
		return path;
	}

	/**
	 * 字符串转码。
	 * @param str 字符串。
	 * @return 转码后的字符串。
	 */
	private String changeEncoding(String str) throws UnsupportedEncodingException {
		str = new String(str.getBytes(localCharset), SERVER_CHARSET);
		return str;
	}

	private String getMsgByCode(int code) {
		switch (code) {
			case FTPReply.RESTART_MARKER:
				return "重新启动标记应答";
			case FTPReply.SERVICE_NOT_READY:
				return "服务不可用";
			case FTPReply.DATA_CONNECTION_ALREADY_OPEN:
				return "数据连接已经打开，开始传送数据";
			case FTPReply.FILE_STATUS_OK:
				return "文件状态正确，正在打开数据连接";
			case FTPReply.COMMAND_OK:
				return "命令执行正常结束";
			case FTPReply.COMMAND_IS_SUPERFLUOUS:
				return "命令未被执行，此站点不支持此命令";
			case FTPReply.SYSTEM_STATUS:
				return "系统状态或系统帮助信息回应";
			case FTPReply.DIRECTORY_STATUS:
				return "目录状态信息";
			case FTPReply.FILE_STATUS:
				return "文件状态信息";
			case FTPReply.HELP_MESSAGE:
				return "帮助信息";
			case FTPReply.NAME_SYSTEM_TYPE:
				return "NAME系统类型";
			case FTPReply.SERVICE_READY:
				return "服务已就绪";
			case FTPReply.SERVICE_CLOSING_CONTROL_CONNECTION:
				return "控制连接关闭";
			case FTPReply.DATA_CONNECTION_OPEN:
				return "数据连接已打开，没有进行中的数据传送";
			case FTPReply.CLOSING_DATA_CONNECTION:
				return "正在关闭数据连接。请求文件动作成功结束";
			case FTPReply.ENTERING_PASSIVE_MODE:
				return "进入被动模式";
			case FTPReply.ENTERING_EPSV_MODE:
				return "进入EPSV模式";
			case FTPReply.USER_LOGGED_IN:
				return "用户登入";
			case FTPReply.FILE_ACTION_OK:
				return "文件操作成功";
			case FTPReply.PATHNAME_CREATED:
				return "路径已创建";
			case FTPReply.NEED_PASSWORD:
				return "请输入密码";
			case FTPReply.NEED_ACCOUNT:
				return "请输入用户名";
			case FTPReply.FILE_ACTION_PENDING:
				return "文件操作需要更多信息";
			case FTPReply.SERVICE_NOT_AVAILABLE:
				return "服务不可用";
			case FTPReply.CANNOT_OPEN_DATA_CONNECTION:
				return "无法打开数据连接";
			case FTPReply.TRANSFER_ABORTED:
				return "传输中止";
			case FTPReply.FILE_ACTION_NOT_TAKEN:
				return "文件操作请求未执行";
			case FTPReply.ACTION_ABORTED:
				return "操作中止，处理过程中发生本地错误";
			case FTPReply.INSUFFICIENT_STORAGE:
				return "存储空间不足";
			case FTPReply.UNRECOGNIZED_COMMAND:
				return "无法识别的命令";
			case FTPReply.SYNTAX_ERROR_IN_ARGUMENTS:
				return "参数语法错误";
			case FTPReply.COMMAND_NOT_IMPLEMENTED:
				return "命令未执行";
			case FTPReply.BAD_COMMAND_SEQUENCE:
				return "命令次序错误";
			case FTPReply.COMMAND_NOT_IMPLEMENTED_FOR_PARAMETER:
				return "由于参数错误导致命令未执行";
			case FTPReply.NOT_LOGGED_IN:
				return "未登录";
			case FTPReply.NEED_ACCOUNT_FOR_STORING_FILES:
				return "存储文件需要用户信息";
			case FTPReply.FILE_UNAVAILABLE:
				return "文件无权访问";
			case FTPReply.PAGE_TYPE_UNKNOWN:
				return "页面类型未知";
			case FTPReply.STORAGE_ALLOCATION_EXCEEDED:
				return "存储空间分配超出";
			case FTPReply.FILE_NAME_NOT_ALLOWED:
				return "文件名非法";
			case FTPReply.SECURITY_DATA_EXCHANGE_COMPLETE:
				return "安全数据交换完成";
			case FTPReply.SECURITY_DATA_EXCHANGE_SUCCESSFULLY:
				return "安全数据交换成功";
			case FTPReply.SECURITY_MECHANISM_IS_OK:
				return "安全策略正常";
			case FTPReply.SECURITY_DATA_IS_ACCEPTABLE:
				return "安全数据读取正常";
			case FTPReply.UNAVAILABLE_RESOURCE:
				return "资源不可用";
			case FTPReply.BAD_TLS_NEGOTIATION_OR_DATA_ENCRYPTION_REQUIRED:
				return "TLS协商失败或者数据需要加密";
			case FTPReply.DENIED_FOR_POLICY_REASONS:
				return "由于协议原因被拒绝访问";
			case FTPReply.REQUEST_DENIED:
				return "请求被拒绝";
			case FTPReply.FAILED_SECURITY_CHECK:
				return "安全检查失败";
			case FTPReply.REQUESTED_PROT_LEVEL_NOT_SUPPORTED:
				return "请求再端口级别不被支持";
		}
		return "";
	}

}